﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Furiant.Figment.Agents.Inventory;

namespace Furiant.Figment.Items.Lootable
{
    /// <summary>
    /// An ILootContainer that can be equipped to a THasInventory implementor.
    /// This class is suitable for representing a container which travels with an Agent, such as a backpack or a ship's cargo hold.
    /// </summary>
    /// <typeparam name="THasInventory"></typeparam>
    public class EquippableLootContainer : Equippable, ILootContainer 
    {
        private readonly List<ILootable> _collection = new List<ILootable>();

        private readonly int? _massMax;
        private readonly int? _itemCountMax;

        /// <summary>
        /// Creates a new instance of EquippableLootContainer.
        /// </summary>
        /// <param name="nameSingle">The player-readable name of a single unit of this instance.</param>
        /// <param name="nameGrouped">The player-readable name of a group of this instance.</param>
        /// <param name="description">The player-readable description of this instance.</param>
        /// <param name="unitMass">The base mass of a single unit of this instance.</param>
        /// <param name="massMax">The maximum value that will be allowed for the sum of the UnitMass values of 
        /// all ILootable instances in the collection. A null value indicates no restriction.</param>
        /// <param name="itemCountMax">The maximum number of ILootable items allowed in the collection.
        /// A null value indicates no restriction.</param>
        /// <param name="equipSlotMaps">The EquipSlotMapCollection that represents the valid equipment slot configurations for
        /// this instance.</param>
        /// <param name="equipActionCriteria">The collection of EquipActionCriterion which must be satisfied in order for a
        /// THasInventory to equip or unequip this instance.</param>
        public EquippableLootContainer(
            string nameSingle, 
            string nameGrouped,
            string description, 
            int unitMass,
            int? massMax,
            int? itemCountMax, 
            EquipSlotMapCollection equipSlotMaps, 
            EquipActionCriterionCollection<IHasInventory> equipActionCriteria)
            : base(nameSingle, nameGrouped, description, unitMass, 1, 1, equipSlotMaps, equipActionCriteria)
        {
            if (massMax.HasValue) _massMax = Math.Max(0, massMax.Value);
            if (itemCountMax.HasValue) _itemCountMax = Math.Max(0, itemCountMax.Value);
        }

        #region Properties

        public virtual ILootable this[int index] { get { return _collection[index]; } }
        
        /// <summary>
        /// Gets the sum of the TotalMass values of all items in the collection.
        /// </summary>
        public virtual int ContentsMassTotal { get { return _collection.Sum(p => p.MassTotal); } }

        /// <summary>
        /// Gets the sum of the TotalMass values of all items in the collection, plus the UnitMass value of this instance.
        /// </summary>
        public override int MassTotal { get { return ContentsMassTotal + UnitMass; } }

        /// <summary>
        /// Gets the maximum value allowed for ContentsMassTotal. A null value indicates no restriction.
        /// </summary>
        public virtual int? MassMax { get { return _massMax; } }

        /// <summary>
        /// Gets the difference of MassMax and ContentsMassTotal. A null value indicates that no restriction is in place.
        /// </summary>
        public virtual int? MassAvailable { get { return _massMax - ContentsMassTotal; } }

        /// <summary>
        /// Gets the number of ILootable instances in the collection.
        /// </summary>
        public virtual int ItemCount { get { return _collection.Count; } }

        /// <summary>
        /// Gets the maximum number of ILootable instances allowed in the collection. A null value indicates no restriction.
        /// </summary>
        public virtual int? ItemCountMax { get { return _itemCountMax; } }

        /// <summary>
        /// Gets the difference of ItemCountMax and ItemCount. A null value indicates that no restriction is in place.
        /// </summary>
        public virtual int? ItemCountAvailable { get { return _itemCountMax - _collection.Count; } }

        /// <summary>
        /// Gets the sum of the UnitCount values of all ILootable instances in the collection.
        /// </summary>
        public virtual int ContentsUnitCountTotal { get { return _collection.Sum(p => p.UnitCount); } }

        #endregion

        #region Methods

        protected virtual void ValidateItemCountMax()
        {
            if (!_itemCountMax.HasValue) return;
            if (_collection.Count >= _itemCountMax)
                throw new InvalidOperationException("This container is full.");
        }

        protected virtual void ValidateMassMax(int value)
        {
            if(!_massMax.HasValue) return;
            if (value > MassAvailable)
                throw new Exception("This container cannot accommodate that much mass.");
        }

        protected virtual void ValidateMassMax(ILootable item)
        {
            if (!_massMax.HasValue) return;
            if (item.MassTotal + ContentsMassTotal > MassAvailable)
                throw new Exception("This container cannot accomodate that much mass.");
        }

        /// <summary>
        /// Adds an object to the end of the collection.
        /// </summary>
        /// <param name="item">The item to add. The item will be automatically grouped with like items if applicable.</param>
        public virtual void Add(ILootable item)
        {
            Add(item, true);
        }

        /// <summary>
        /// Adds an object to the end of the collection.
        /// </summary>
        /// <param name="item">The item to add.</param>
        /// <param name="autoGroup">Whether the item should be automatically grouped with like items if possible. This operation will find
        /// any objects of the same type whose UnitCount value is less than their UnitsPerGroup value, and transfer units from the specified item
        /// to each found object until the found object's capacity is reached, repeating until no found objects remain who can accept more units or 
        /// until no units remain in the specified item. If any units remain in the specified item, it will be added to the collection.</param>
        public virtual void Add(ILootable item, bool autoGroup)
        {
            ValidateItemCountMax();
            ValidateMassMax(item);

            if (autoGroup)
            {
                var foundGroups = GetLikeGroups(item, true);

                foreach (var group in foundGroups)
                {
                    item.TransferUnits(group, item.UnitCount);

                    if (item.UnitCount < 1) return;
                }
            }

            _collection.Add(item);
            
            ProcessCollectionChange(item, ContainerItemMoveMode.Incoming);
        }

        protected virtual IEnumerable<ILootable> GetLikeGroups(ILootable item, bool ignoreFull)
        {
            var groups = _collection.Where(p => p.GetType() == item.GetType()).ToList();

            return ignoreFull ? groups.Where(p => p.UnitsPerGroup > p.UnitCount) : groups;
        }

        /// <summary>
        /// Inserts an element into the collection at the specified index.
        /// </summary>
        /// <param name="index">The zero-based index at which the item should be inserted.</param>
        /// <param name="item">The object to insert. This value cannot be null.</param>
        public virtual void Insert(int index, ILootable item)
        {
            if (item == null) throw new ArgumentNullException("item");

            ValidateItemCountMax();
            ValidateMassMax(item);

            _collection.Insert(index, item);

            ProcessCollectionChange(item, ContainerItemMoveMode.Incoming);
        }

        public virtual void Clear()
        {
            foreach(var item in _collection)
            {
                ProcessCollectionChange(item, ContainerItemMoveMode.Outgoing);
            }
            
            _collection.Clear();
        }

        /// <summary>
        /// Determines whether an element is in the collection.
        /// </summary>
        /// <param name="item">The object to locate in the collection.</param>
        /// <returns></returns>
        public virtual bool Contains(ILootable item)
        {
            return _collection.Contains(item);
        }

        /// <summary>
        /// Removes the first occurrence of a specific object from the collection.
        /// </summary>
        /// <param name="item">The object to remove from the collection.</param>
        public virtual bool Remove(ILootable item)
        {
            var removed = _collection.Remove(item);

            if (!removed) return false;

            ProcessCollectionChange(item, ContainerItemMoveMode.Outgoing);

            return true;
        }

        /// <summary>
        /// Searches for the specified object and returns the zero-based index of the first occurrence 
        /// within the entire collection.
        /// </summary>
        /// <param name="item">The object to locate in the collection.</param>
        public virtual int IndexOf(ILootable item)
        {
            return _collection.IndexOf(item);
        }

        /// <summary>
        /// Searches for an element that matches the conditions defined by the specified predicate, 
        /// and returns the first occurrence within the entire collection.
        /// </summary>
        /// <param name="match">The delegate that defines the conditions of the object to search for.</param>
        public virtual ILootable Find(Predicate<ILootable> match)
        {
            return _collection.Find(match);
        }

        IEnumerator<ILootable> IEnumerable<ILootable>.GetEnumerator()
        {
            return GetEnumerator();
        }

        public virtual IEnumerator<ILootable> GetEnumerator()
        {
            return _collection.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region Events

        protected virtual void ProcessCollectionChange(ILootable item, ContainerItemMoveMode mode)
        {
            switch (mode)
            {
                case ContainerItemMoveMode.Incoming:
                    {
                        item.CurrentInventory = CurrentInventory; 
                        // Incoming item gets its CurrentInventory assigned by this container
                        // This triggers ItemInventoryChanged event in previous container
                        item.InventoryChanged += HandleItemInventoryChanged;
                        item.UnitCountChanged += HandleItemUnitCountChanged;
                        break;
                    }
                case ContainerItemMoveMode.Outgoing:
                    {
                        // Outgoing item will get its CurrentInventory assigned by the next receiving container
                        // This will trigger the ItemInventoryChanged event in this instance
                        item.InventoryChanged -= HandleItemInventoryChanged;
                        item.UnitCountChanged -= HandleItemUnitCountChanged;
                        break;
                    }
            }

            OnContainerContentsChanged(new LootContainerContentsChangedArgs(item, mode));
            OnContainerSumsChanged(new LootContainerSumsChangedArgs(this));
        }

        protected virtual void HandleItemUnitCountChanged(object sender, UnitCountChangedArgs e)
        {
            var s = (ILootable)sender;

            if (s.UnitCount < 1)
            {
                Remove(s); // Calls ProcessCollectionChange
            }
            else
            {
                OnContainerSumsChanged(new LootContainerSumsChangedArgs(this));
            }
        }

        protected virtual void HandleItemInventoryChanged(object sender, InventoryChangedArgs e)
        {
            var s = (ILootable)sender;

            ProcessCollectionChange(s, ContainerItemMoveMode.Outgoing);
        }

        public event EventHandler<LootContainerContentsChangedArgs> ContainerContentsChanged;

        protected virtual void OnContainerContentsChanged(LootContainerContentsChangedArgs e)
        {
            var handler = ContainerContentsChanged;
            if (handler == null) return;

            handler(this, e);
        }

        public event EventHandler<LootContainerSumsChangedArgs> ContainerSumsChanged;

        protected virtual void OnContainerSumsChanged(LootContainerSumsChangedArgs e)
        {
            var handler = ContainerSumsChanged;
            if (handler == null) return;

            handler(this, e);
        }

        #endregion
    }
}
